Skip to content

fix(check-watch-hint-literal): red on a declared watch-hint literal the extractor drops at admission - #16799

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-16447-watch-hint-admission-finding
Sep 8, 2026
Merged

fix(check-watch-hint-literal): red on a declared watch-hint literal the extractor drops at admission#16799
hotlong merged 2 commits into
mainfrom
claude/issue-16447-watch-hint-admission-finding

Conversation

@hotlong

@hotlong hotlong commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #16447

check:watch-hint-literal now reds on a *_WATCH_HINTS literal that the hint extractor's
admission silently drops, names the remedy, and carries the measurement behind the refusal.
The admission rule itself is untouched — this is triage's option 2, and nothing that is
refused today becomes accepted.

What was silent

A watch-hint declaration can be a perfect literal array and still contribute nothing:
extractWatchHints admits a literal only when it STARTS with a word character, a dot or an
@, so one opening with a glob never reaches the resolve step, never becomes a hint, and
places its gate on NO card. Four instruments passed that drop, because each asked its own
question and none asked whether the literals were admissible.

declaration extractWatchHints
const ROOT_DIR_WATCH_HINTS = ['**/package.json']; []
the four literals PR #16446 enumerates instead all four

The sweep now asks the second question, of the extractor rather than of a copy of its
rule — a second copy of an admission regex is a thing that drifts, and it would drift in the
same silent direction.

Why one literal at a time, rather than a whole-module diff

The obvious spelling is to extract the whole module and diff the declared literals against
the result. Measured on this tree, that spelling is wrong in both directions:

  • it falsely accuses scripts/pm/dispatch-gates.mjs, which spells a rostered declaration
    inside its own self-test as a fixture string — the extractor blanks self-tests, so a
    perfectly admissible literal is absent from that module's extraction. Measured: 1 of 151
    live declared literals falsely flagged by the whole-module diff, 0 by the per-literal probe;
  • it would falsely accuse again on any declaration written module-relative, whose extracted
    hint is the RESOLVED path and not the literal as spelled.

Probing one literal at a time asks exactly the question the finding is about — does this
literal produce a hint at all — and neither residue reaches it.

验收备注

Answering triage's five acceptance items one by one (ruling comment 5579014475).

1. finding 必须先红. The known positive was added to the self-test FIRST and run against
the UNFIXED implementation, which does not report it:

$ node scripts/check-watch-hint-literal.mjs --self-test      # commit 93f311eb6, finding class absent
  ✗ a declaration whose only literal opens with a glob is rejected
✗ check-watch-hint-literal self-test: 1 of 58 case(s) failed.

With the finding class in place (commit 59a0adb) the same case passes, and the whole battery
is 14 cases. An ablation from the committed state confirms it is the finding class doing the
work, not the fixture: deleting it on disk (blob b129dfe86d9673fc; anchor occurrences
1 → 0, injected marker 0 → 1) turns 7 cases red; restoring with git checkout HEAD -- puts the
blob back at b129dfe8 with an empty git diff HEAD and 0 leftover markers, and the self-test
returns to 72 passing cases.

2. finding 的文案要说清补救. The remedy text names the dropped literal, states the
admission rule that dropped it, prescribes the enumerable root-prefixed spelling plus the pin
against the gate's own walk, cites the measurement — +139084 fabricated pairs for
bare-top-level-word admission, and the re-measured refusal of the resolved-form widening — and
says relaxing it is a change to every gate in the farm rather than a repair to one declaration.
It closes with the ruling's own sentence in the checker's register: "This finding does not
solve the trouble of enumerating; it solves not knowing that you need to enumerate."
Five
self-test cases assert those five properties of the text, so it cannot decay into "not legal".

3. 阴性对照必测.

  • PR feat(scripts): gate repository.directory against the manifest's own directory #16446's four admissible literals are accepted, together and individually — two self-test
    cases, plus pnpm check:manifest-repository-directory green.
  • A zero-literal declaration keeps its OWN remedy (names no subtree at all) and a computed one
    keeps its own (is COMPUTED, not a literal array); the value-side question is asked only after
    the spelling-side one passes, so no declaration is named under two remedies. Two self-test cases.
  • extractWatchHints over the derivation's whole corpus: 225 of 226 gate files byte-identical,
    280 of 281 family hint-sets byte-identical. The one delta is this gate's own entry, and it is
    the import of the extractor it now consults — see Declared deviation below, with the measurement
    that placement is unchanged.

4. ⛔ 不动 dispatch-gates.mjs 的 admission 正则. Untouched — the diff is two files and
neither is it. Nothing that fails admission today is admitted by this PR; the only thing that
changed is whether the failure is reported.

5. check-manifest-repository-directory.mjs's docblock. No longer accurate, and edited —
only that sentence. It called the drop silent; after this PR it is not. The paragraph now says
so and points at the remedy the gate prints.

Declared deviation — one hint, and it does not place anything

Consulting the extractor means importing it, and the import specifier is itself a path-shaped
literal in this gate's module body, so this gate's own extraction gains scripts/pm/dispatch-gates.mjs
(and, through the import follow, .github/workflows). Measured before and after:

scripts/check-watch-hint-literal.mjs   ["scripts/**"]
                                    -> ["scripts/pm/dispatch-gates.mjs","scripts/**"]
check:watch-hint-literal (family)      ["scripts/**"]
                                    -> [".github/workflows","scripts/**","scripts/pm/dispatch-gates.mjs"]

Acceptance item 3's third bullet asks for byte-identical extraction over every gate; this is the
one place it is not, and it is structural — a gate that calls the extractor cannot be invisible to
it. What the bullet's second half asks for does hold, and it is measured rather than argued:
check:watch-hint-literal declares dispatch-gates: whole-tree-population, so it is placed by
declaration and never by its hints — it is in the alwaysRunsPopulation column for every card and
in matched for none. Across --json runs before and after, on probe paths chosen to expose the
delta (scripts/pm/dispatch-gates.mjs, scripts/check-watch-hint-literal.mjs,
packages/spec/src/index.ts, and separately .github/workflows/lint.yml):

matched column        byte-identical (81 families)
alwaysRunsPopulation  identical (7)      widePopulation identical (10)
counts                identical          runnable union identical (82 commands)

⇒ no card's brief moves. The alternative — routing the import through a shim module so the
specifier carries no slash — was rejected: a file whose only purpose is to be invisible to a
detector is the workaround Prime Directive #5 refuses, and the next author would copy it.

Verification

Re-derived in the worktree at 59a0adb3c with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) —
31 families, the 30 the dispatch named plus pnpm check:manifest-repository-directory, which
arrived because this diff now touches that file. All 31 run; reconciled with --ran:

✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED.

pnpm check:pm-dispatch-gates exit 0 (✓ dispatch-gates self-test: 1552 cases pass.).
pnpm check:watch-hint-literal exit 0 — 72 self-test cases, then 66 declarations across 4
rostered names with all 151 of their literals admitted.

Lint, as a proven narrowing — three readings, taken at 59a0adb3c:

  1. the population is read from eslint's own config, not guessed: 6347 tracked JS/TS files, and
    ESLint#isPathIgnored says 6347 of 6347 are not ignored;
  2. files actually linted, counted from --format json: 2, 0 errors, 0 warnings;
  3. invariance: eslint.config.mjs never enables type-aware linting for ANY file —
    calculateConfigForFile on the changed file returns
    parserOptions {"ecmaVersion":"latest","sourceType":"module"}, no parserOptions.project, no
    typed rules — so this diff cannot move the verdict on any untouched file.

Merge-tree against origin/main b38821d1c, probed from a throwaway bare clone with no custom
merge driver registered: exit 0, no conflicted paths.
node scripts/pm/check-governed-merges.mjs --test on the final file list: NOT governed.
skip-changeset: the repo-root package is private (@objectstack/spec-monorepo), the two changed
files sit outside every workspace package, and the new symbol has zero occurrences anywhere under
packages/ while the positive control (defineStack in packages/spec/src/index.ts, 6 hits) shows
the grep channel works — nothing published moves.

Deliberately out of scope, per the ruling: widening admission to accept a leading glob (triage's
option 1) is a separate card and a decision-box one, because it changes the derivation's admission
semantics for every gate in the farm and owes an answer to the +139084 measurement. No card is
opened for it here.


Generated by Claude Code

…inst the unfixed implementation

Records acceptance item 1's first half: with the case present and the finding
class absent, the self-test reports the case as failing -- the drop is not
reported by the unfixed checker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY
…ractor drops

A watch-hint declaration can be a perfect literal array and still contribute
nothing: extractWatchHints admits a literal only when it starts with a word
character, a dot or an @, so one opening with a glob never becomes a hint and
places its gate on no card. Four instruments passed that drop because each
asked its own question and none asked whether the literals were admissible.

The sweep now asks the second question, of the extractor rather than of a copy
of its rule, one literal at a time -- a whole-module diff falsely accuses a
literal spelled inside a self-test the extractor blanks, and one written
module-relative whose extracted hint is the resolved path. The remedy names the
dropped literal, prescribes the enumerable root-prefixed spelling, and carries
the +139084-pair measurement behind the refusal so widening admission does not
read as the obvious repair.

The admission rule itself is untouched. The manifest gate's docblock stops
calling this drop silent, because it no longer is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY
@github-actions github-actions Bot added the size/m label Sep 8, 2026
@hotlong hotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 8, 2026 — with Claude
@hotlong
hotlong marked this pull request as ready for review September 8, 2026 06:20
@hotlong
hotlong enabled auto-merge September 8, 2026 06:20
@hotlong
hotlong added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 83863b2 Sep 8, 2026
36 checks passed
@hotlong
hotlong deleted the claude/issue-16447-watch-hint-admission-finding branch September 8, 2026 06:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants